﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;

namespace SunnyChen.Common.Windows.ShellApi
{
    public class ShellBrowseForFolderDialog
    {
        // The SendMessage function sends the specified message to a window or windows. It calls the window procedure for 
        // the specified window and does not return until the window procedure has processed the message. 
        [DllImport("User32.dll")]
        public static extern Int32 SendMessage(
            IntPtr hWnd,			// handle to destination window
            UInt32 Msg,				// message
            UInt32 wParam,			// first message parameter
            Int32 lParam			// second message parameter
            );

        [DllImport("User32.dll")]
        public static extern Int32 SendMessage(
            IntPtr hWnd,			// handle to destination window
            UInt32 Msg,				// message
            UInt32 wParam,			// first message parameter
            [MarshalAs(UnmanagedType.LPWStr)]
			String lParam			// second message parameter
            );

        public enum RootTypeOptions
        {
            BySpecialFolder,
            ByPath
        }

        [Flags]
        public enum BrowseInfoFlag // BIF
        {
            BIF_RETURNONLYFSDIRS = 0x0001,	// For finding a folder to start document searching
            BIF_DONTGOBELOWDOMAIN = 0x0002,	// For starting the Find Computer
            BIF_STATUSTEXT = 0x0004,	// Top of the dialog has 2 lines of text for BROWSEINFO.lpszTitle and 
            // one line if this flag is set.  Passing the message 
            // BFFM_SETSTATUSTEXTA to the hwnd can set the rest of the text.  
            // This is not used with BIF_USENEWUI and BROWSEINFO.lpszTitle gets
            // all three lines of text.
            BIF_RETURNFSANCESTORS = 0x0008,
            BIF_EDITBOX = 0x0010,	// Add an editbox to the dialog
            BIF_VALIDATE = 0x0020,	// insist on valid result (or CANCEL)
            BIF_NEWDIALOGSTYLE = 0x0040,	// Use the new dialog layout with the ability to resize
            // Caller needs to call OleInitialize() before using this API
            BIF_USENEWUI = (BIF_NEWDIALOGSTYLE | BIF_EDITBOX),
            BIF_BROWSEINCLUDEURLS = 0x0080,    // Allow URLs to be displayed or entered. (Requires BIF_USENEWUI)
            BIF_UAHINT = 0x0100,    // Add a UA hint to the dialog, in place of the edit box. May not be 
            // combined with BIF_EDITBOX
            BIF_NONEWFOLDERBUTTON = 0x0200,    // Do not add the "New Folder" button to the dialog.  Only applicable 
            // with BIF_NEWDIALOGSTYLE.
            BIF_NOTRANSLATETARGETS = 0x0400,    // don't traverse target as shortcut
            BIF_BROWSEFORCOMPUTER = 0x1000,	// Browsing for Computers.
            BIF_BROWSEFORPRINTER = 0x2000,	// Browsing for Printers
            BIF_BROWSEINCLUDEFILES = 0x4000,	// Browsing for Everything
            BIF_SHAREABLE = 0x8000		// sharable resources displayed (remote shares, requires BIF_USENEWUI)
        }

        public enum BrowseForFolderMessages	// BFFM
        {
            // message from browser
            BFFM_INITIALIZED = 1,
            BFFM_SELCHANGED = 2,
            BFFM_VALIDATEFAILEDA = 3,				// lParam:szPath ret:1(cont),0(EndDialog)
            BFFM_VALIDATEFAILEDW = 4,				// lParam:wzPath ret:1(cont),0(EndDialog)
            BFFM_IUNKNOWN = 5, 				// provides IUnknown to client. lParam: IUnknown*

            // messages to browser
            // 0x400 = WM_USER
            BFFM_SETSTATUSTEXTA = (0x0400 + 100),
            BFFM_ENABLEOK = (0x0400 + 101),
            BFFM_SETSELECTIONA = (0x0400 + 102),
            BFFM_SETSELECTIONW = (0x0400 + 103),
            BFFM_SETSTATUSTEXTW = (0x0400 + 104),
            BFFM_SETOKTEXT = (0x0400 + 105),	// Unicode only
            BFFM_SETEXPANDED = (0x0400 + 106)	// Unicode only
        }

        public class InitializedEventArgs : EventArgs
        {
            public InitializedEventArgs(IntPtr hwnd)
            {
                this.hwnd = hwnd;
            }
            public readonly IntPtr hwnd;
        }

        public class IUnknownEventArgs : EventArgs
        {
            public IUnknownEventArgs(IntPtr hwnd, IntPtr iunknown)
            {
                this.hwnd = hwnd;
                this.iunknown = iunknown;
            }
            public readonly IntPtr hwnd;
            public readonly IntPtr iunknown;
        }

        public class SelChangedEventArgs : EventArgs
        {
            public SelChangedEventArgs(IntPtr hwnd, IntPtr pidl)
            {
                this.hwnd = hwnd;
                this.pidl = pidl;
            }
            public readonly IntPtr hwnd;
            public readonly IntPtr pidl;
        }

        public class ValidateFailedEventArgs : EventArgs
        {
            public ValidateFailedEventArgs(IntPtr hwnd, string invalidSel)
            {
                this.hwnd = hwnd;
                this.invalidSel = invalidSel;
            }
            public readonly IntPtr hwnd;
            public readonly string invalidSel;
        }


        public delegate void InitializedHandler(ShellBrowseForFolderDialog sender, InitializedEventArgs args);
        public delegate void IUnknownHandler(ShellBrowseForFolderDialog sender, IUnknownEventArgs args);
        public delegate void SelChangedHandler(ShellBrowseForFolderDialog sender, SelChangedEventArgs args);
        public delegate int ValidateFailedHandler(ShellBrowseForFolderDialog sender, ValidateFailedEventArgs args);

        public event InitializedHandler OnInitialized;
        public event IUnknownHandler OnIUnknown;
        public event SelChangedHandler OnSelChanged;
        public event ValidateFailedHandler OnValidateFailed;

        public void EnableOk(IntPtr hwnd, bool Enabled)
        {
            SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_ENABLEOK, 0, Enabled ? 1 : 0);
        }

        public void SetExpanded(IntPtr hwnd, string path)
        {
            SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETEXPANDED, 1, path);
        }

        public void SetOkText(IntPtr hwnd, string text)
        {
            SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETOKTEXT, 0, text);
        }

        public void SetSelection(IntPtr hwnd, string path)
        {
            SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSELECTIONW, 1, path);
        }

        public void SetStatusText(IntPtr hwnd, string text)
        {
            SendMessage(hwnd, (uint)BrowseForFolderMessages.BFFM_SETSTATUSTEXTW, 1, text);
        }


        private Int32 myBrowseCallbackProc(IntPtr hwnd, UInt32 uMsg, Int32 lParam, Int32 lpData)
        {
            switch ((BrowseForFolderMessages)uMsg)
            {
                case BrowseForFolderMessages.BFFM_INITIALIZED:
                    System.Diagnostics.Debug.WriteLine("BFFM_INITIALIZED");

                    if (OnInitialized != null)
                    {
                        InitializedEventArgs args = new InitializedEventArgs(hwnd);
                        OnInitialized(this, args);
                    }

                    break;

                case BrowseForFolderMessages.BFFM_IUNKNOWN:
                    System.Diagnostics.Debug.WriteLine("BFFM_IUNKNOWN");

                    if (OnIUnknown != null)
                    {
                        IUnknownEventArgs args = new IUnknownEventArgs(hwnd, (IntPtr)lParam);
                        OnIUnknown(this, args);
                    }

                    break;

                case BrowseForFolderMessages.BFFM_SELCHANGED:
                    System.Diagnostics.Debug.WriteLine("BFFM_SELCHANGED");

                    if (OnSelChanged != null)
                    {
                        SelChangedEventArgs args = new SelChangedEventArgs(hwnd, (IntPtr)lParam);
                        OnSelChanged(this, args);
                    }

                    break;

                case BrowseForFolderMessages.BFFM_VALIDATEFAILEDA:
                    System.Diagnostics.Debug.WriteLine("BFFM_VALIDATEFAILEDA");

                    if (OnValidateFailed != null)
                    {
                        string failedSel = Marshal.PtrToStringAnsi((IntPtr)lParam);
                        ValidateFailedEventArgs args = new ValidateFailedEventArgs(hwnd, failedSel);
                        return OnValidateFailed(this, args);
                    }
                    break;

                case BrowseForFolderMessages.BFFM_VALIDATEFAILEDW:
                    System.Diagnostics.Debug.WriteLine("BFFM_VALIDATEFAILEDW");

                    if (OnValidateFailed != null)
                    {
                        string failedSel = Marshal.PtrToStringUni((IntPtr)lParam);
                        ValidateFailedEventArgs args = new ValidateFailedEventArgs(hwnd, failedSel);
                        return OnValidateFailed(this, args);
                    }

                    break;
            }

            return 0;
        }


        public ShellBrowseForFolderDialog()
        {
            hwndOwner = IntPtr.Zero;
            RootType = RootTypeOptions.BySpecialFolder;
            RootSpecialFolder = ShellApi.CSIDL.CSIDL_DESKTOP;
            RootPath = "";
            m_DisplayName = "";
            Title = "";
            UserToken = IntPtr.Zero;
            m_FullName = "";

            // Default flags values
            DetailsFlags = BrowseInfoFlag.BIF_BROWSEINCLUDEFILES
                | BrowseInfoFlag.BIF_EDITBOX
                | BrowseInfoFlag.BIF_NEWDIALOGSTYLE
                | BrowseInfoFlag.BIF_SHAREABLE
                | BrowseInfoFlag.BIF_STATUSTEXT
                | BrowseInfoFlag.BIF_USENEWUI
                | BrowseInfoFlag.BIF_VALIDATE;

        }

        public void ShowDialog()
        {
            m_FullName = "";
            m_DisplayName = "";

            // Get shell's memory allocator, it is needed to free some memory later
            IMalloc pMalloc;
            pMalloc = ShellFunctions.GetMalloc();

            IntPtr pidlRoot;

            if (RootType == RootTypeOptions.BySpecialFolder)
            {
                ShellApi.SHGetFolderLocation(hwndOwner, (int)RootSpecialFolder, UserToken, 0, out pidlRoot);
            }
            else	// m_RootType = RootTypeOptions.ByPath
            {
                uint iAttribute;
                ShellApi.SHParseDisplayName(RootPath, IntPtr.Zero, out pidlRoot, 0, out iAttribute);
            }

            ShellApi.BROWSEINFO bi = new ShellApi.BROWSEINFO();

            bi.hwndOwner = hwndOwner;
            bi.pidlRoot = pidlRoot;
            bi.pszDisplayName = new String(' ', 256);
            bi.lpszTitle = Title;
            bi.ulFlags = (uint)DetailsFlags;
            bi.lParam = 0;
            bi.lpfn = new ShellApi.BrowseCallbackProc(this.myBrowseCallbackProc);

            // Show dialog
            IntPtr pidlSelected;
            pidlSelected = ShellApi.SHBrowseForFolder(ref bi);

            // Save the display name
            m_DisplayName = bi.pszDisplayName.ToString();

            IShellFolder isf = ShellFunctions.GetDesktopFolder();

            ShellApi.STRRET ptrDisplayName;
            isf.GetDisplayNameOf(pidlSelected, (uint)ShellApi.SHGNO.SHGDN_NORMAL | (uint)ShellApi.SHGNO.SHGDN_FORPARSING, out ptrDisplayName);

            String sDisplay;
            ShellApi.StrRetToBSTR(ref ptrDisplayName, pidlRoot, out sDisplay);
            m_FullName = sDisplay;

            if (pidlRoot != IntPtr.Zero)
                pMalloc.Free(pidlRoot);

            if (pidlSelected != IntPtr.Zero)
                pMalloc.Free(pidlSelected);

            Marshal.ReleaseComObject(isf);
            Marshal.ReleaseComObject(pMalloc);
        }


        ///  <summary> Handle to the owner window for the dialog box.  </summary>
        public IntPtr hwndOwner;

        /// <summary> Select the root type </summary>
        public RootTypeOptions RootType;

        /// <summary> valid only if RootType is RootTypeOptions.ByPath </summary>
        public string RootPath;

        /// <summary> valid only if RootType is RootTypeOptions.BySpecialFolder </summary>
        public ShellApi.CSIDL RootSpecialFolder;

        /// <summary> Address of a buffer to receive the display name of the folder selected by the user. </summary>
        public string DisplayName
        {
            get
            {
                return m_DisplayName;
            }
        }
        private string m_DisplayName;

        /// <summary> 
        /// Address of a null-terminated string that is displayed above the tree view control in the dialog box. 
        /// </summary>
        public string Title;

        /// <summary> Token that can be used to represent a particular user. </summary>
        public IntPtr UserToken;

        /// <summary> Return the result of the dialog </summary>
        public string FullName
        {
            get
            {
                return m_FullName;
            }
        }
        private string m_FullName;

        public BrowseInfoFlag DetailsFlags;

    }
}
